﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using System.Xml.Schema;
using System.Xml;
using System.Collections;
using System.Web.Services.Description;
using System.Web;
using Thinktecture.Tools.Wscf.Services.ServiceDescription;
using System.Collections.Specialized;

namespace wscf.lightblue
{
	/// <summary>
	/// WCF Code Generator
	/// </summary>
	public class CodeGenerator
	{
		/// <summary>
		/// Expands the schemas to include all imported schemas.
		/// </summary>
		/// <param name="metadataFiles">The metadata files.</param>
		/// <returns>List of all referenced schemas (no duplicates)</returns>
		static public List<string> ExpandSchemas(string metadataFiles)
		{
			List<string> metadataFileList = new List<string>();
			List<string> fileList = new List<string>();
			if (metadataFiles.Contains(';'))
			{
				foreach (string metadataFile in metadataFiles.Split(';'))
				{
					if (!metadataFileList.Contains(metadataFile))
					{
						metadataFileList.Add(metadataFile);
					}
				}
			}
			else
			{
				metadataFileList.Add(metadataFiles);
			}
			Hashtable schemas = new Hashtable();
			foreach (string metadataFile in metadataFileList)
			{
				if (metadataFile.Contains("*"))
				{
					string path = Path.GetDirectoryName(metadataFile);
					if (path.Trim() == string.Empty)
					{
						path = ".";
					}
					if (Directory.Exists(path))
					{
						IEnumerable<string> ies = Directory.EnumerateFiles(path, Path.GetFileName(metadataFiles));
						if (ies != null)
						{
							foreach (string file in ies.ToArray<string>())
							{
								if (!fileList.Contains(file))
								{
									fileList.Add(file);
								}
							}
						}
					}
				}
				else
				{
					if (!fileList.Contains(metadataFile))
					{
						fileList.Add(metadataFile);
					}
				}
			}
			foreach (string file in fileList)
			{
				if (File.Exists(file))
				{
					if (Path.GetExtension(file).ToLower() == ".xsd")
					{
						GetSchemasFromXsd(file, schemas);
					}
					else if (Path.GetExtension(file).ToLower() == ".wsdl")
					{
						GetSchemasFromWsdl(file, schemas);
					}
				}
				else
				{
					schemas.Add(file.ToLower(), file);
				}
			}
			if (schemas.Count == 0)
			{
				schemas.Add(metadataFiles, metadataFiles);
			}
			List<string> list = new List<string>();
			foreach (string s in schemas.Values)
			{
				list.Add(s);
			}
			return list;
		}

		/// <summary>
		/// Generates WCF code from meta data files (.XSD and .WSDL). Calls ScvUtil.exe under the hood.
		/// </summary>
		/// <param name="metadataFiles">The metadata files.</param>
		/// <param name="outputFile">The output file.</param>
		/// <param name="language">The language.</param>
		/// <param name="xmlNamespaceMappings">The XML namespace mappings.</param>
		/// <param name="referencedAssemblies">The referenced assemblies.</param>
		/// <param name="dataContractsOnly">if set to <c>true</c> [data contracts only].</param>
		/// <param name="configurationFile">The server configuration file.</param>
		static public void GenerateCode(string metadataFiles, string outputFile, ClrLanguage language,
			Dictionary<string, string> xmlNamespaceMappings, List<string> referencedAssemblies,
			bool dataContractsOnly, string configurationFile, bool dataContractSerializer)
		{
			string cmdFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
				"wscf.lightblue.CodeGenerator.GenerateCode.cmd");
			List<string> schemas = ExpandSchemas(metadataFiles);
			GenerateCommandFile(cmdFile, schemas, outputFile, xmlNamespaceMappings, referencedAssemblies,
				dataContractsOnly, language, configurationFile, dataContractSerializer);
			RunCommandFile(cmdFile);
			RemoveDelayedBinding(outputFile);
		}

		static public string GenerateWsdl(string serviceName, string nameSpace, string fileName,
			List<string> schemas, string soapInHeaderElementName, string soapOutHeaderElementName, string soapFaultElementName)
		{
			InterfaceContract ic = new InterfaceContract();
			ic.ServiceName = serviceName;
			ic.ServiceNamespace = nameSpace;
			ic.NeedsServiceElement = true;

			NameValueCollection nvc = new NameValueCollection();
			foreach (string schema in schemas)
			{
				if (Path.GetExtension(schema).ToLower() == ".xsd")
				{
					nvc.Add(Path.GetFileName(schema), schema);
				}
			}
			if (nvc.Count > 0)
			{
				ic.AddSchemasToContract(nvc, Environment.CurrentDirectory, Environment.CurrentDirectory);
				ic.InferOperations();

				SchemaElement soapInSchemaElement = null;
				SchemaElement soapOutSchemaElement = null;
				SchemaElement soapFaultSchemaElement = null;
				foreach (SchemaElement se in ic.HeaderSchemas)
				{
					if (!string.IsNullOrEmpty(soapInHeaderElementName) && se.ElementName.Contains(soapInHeaderElementName))
					{
						soapInSchemaElement = se;
					}
					if (!string.IsNullOrEmpty(soapOutHeaderElementName) && se.ElementName.Contains(soapOutHeaderElementName))
					{
						soapOutSchemaElement = se;
					}
					if (!string.IsNullOrEmpty(soapFaultElementName) && se.ElementName.Contains(soapFaultElementName))
					{
						soapFaultSchemaElement = se;
					}
				}

				Thinktecture.Tools.Wscf.Services.ServiceDescription.Operation requestHeaderOp = null;
				Thinktecture.Tools.Wscf.Services.ServiceDescription.Operation responseHeaderOp = null;
				Thinktecture.Tools.Wscf.Services.ServiceDescription.Operation faultOp = null;
				foreach (Thinktecture.Tools.Wscf.Services.ServiceDescription.Operation op in ic.Operations)
				{
					if (op.Name == soapInHeaderElementName)
					{
						requestHeaderOp = op;
					}
					if (op.Name == soapOutHeaderElementName)
					{
						responseHeaderOp = op;
					}
					if (op.Name == soapFaultElementName)
					{
						faultOp = op;
					}
					if (((object)soapInSchemaElement) != null)
					{
						op.MessagesCollection[0].HeadersCollection.Add(new MessageHeader()
						{
							Message = op.Name + "InHeader",
							Name = op.Name + "InHeader"
						});
						op.MessagesCollection.Add(new Thinktecture.Tools.Wscf.Services.ServiceDescription.Message()
						{
							Name = op.Name + "InHeader",
							Documentation = "",
							Element = soapInSchemaElement
						});
					}
					if (((object)soapOutSchemaElement) != null)
					{
						op.MessagesCollection[1].HeadersCollection.Add(new MessageHeader()
						{
							Message = op.Name + "OutHeader",
							Name = op.Name + "OutHeader"
						});
						op.MessagesCollection.Add(new Thinktecture.Tools.Wscf.Services.ServiceDescription.Message()
						{
							Name = op.Name + "OutHeader",
							Documentation = "",
							Element = soapOutSchemaElement
						});
					}
					if (((object)soapFaultSchemaElement) != null)
					{
						op.Faults.Add(new Thinktecture.Tools.Wscf.Services.ServiceDescription.Message()
						{
							Name = op.Name + "Fault",
							Documentation = "",
							Element = soapFaultSchemaElement
						});
					}
				}
				if (requestHeaderOp != null)
				{
					ic.Operations.Remove(requestHeaderOp);
				}
				if (responseHeaderOp != null)
				{
					ic.Operations.Remove(responseHeaderOp);
				}
				if (faultOp != null)
				{
					ic.Operations.Remove(faultOp);
				}

				if (string.IsNullOrEmpty(ic.ServiceNamespace) && ic.MessageSchemas != null && ic.MessageSchemas.Count > 0)
				{
					ic.ServiceNamespace = ic.MessageSchemas[0].ElementNamespace;
				}

				string rootFolder = Path.GetDirectoryName(fileName);
				if (string.IsNullOrEmpty(rootFolder))
				{
					rootFolder = Environment.CurrentDirectory;
				}

				foreach (SchemaImport si in ic.Imports)
				{
					si.SchemaLocation = RelativePathTo(rootFolder, si.SchemaLocation);
				}

				string generatedfile = ServiceDescriptionEngine.GenerateWsdl(ic, rootFolder, "Generated by wscf.lightblue");
				if (generatedfile.ToLower() != fileName.ToLower())
				{
					if (File.Exists(fileName))
					{
						File.Delete(fileName);
					}
					File.Move(generatedfile, fileName);
				}
			}
			return fileName;
		}

		/// <summary>
		/// Creates a relative path from one file
		/// or folder to another.
		/// </summary>
		/// <param name="fromDirectory">
		/// Contains the directory that defines the 
		/// start of the relative path.
		/// </param>
		/// <param name="toPath">
		/// Contains the path that defines the
		/// endpoint of the relative path.
		/// </param>
		/// <returns>
		/// directory to the end path.
		/// </returns>
		/// <exception cref="ArgumentNullException"></exception>
		static private string RelativePathTo(
				string fromDirectory, string toPath)
		{
			if (fromDirectory == null)
				throw new ArgumentNullException("fromDirectory");

			if (toPath == null)
				throw new ArgumentNullException("toPath");

			bool isRooted = Path.IsPathRooted(fromDirectory)
					&& Path.IsPathRooted(toPath);

			if (isRooted)
			{
				bool isDifferentRoot = string.Compare(
						Path.GetPathRoot(fromDirectory),
						Path.GetPathRoot(toPath), true) != 0;

				if (isDifferentRoot)
					return toPath;
			}

			StringCollection relativePath = new StringCollection();
			string[] fromDirectories = fromDirectory.Split(
					Path.DirectorySeparatorChar);

			string[] toDirectories = toPath.Split(
					Path.DirectorySeparatorChar);

			int length = Math.Min(
					fromDirectories.Length,
					toDirectories.Length);

			int lastCommonRoot = -1;

			// find common root
			for (int x = 0; x < length; x++)
			{
				if (string.Compare(fromDirectories[x],
						toDirectories[x], true) != 0)
					break;

				lastCommonRoot = x;
			}

			if (lastCommonRoot == -1)
				return toPath;

			// add relative folders in from path
			for (int x = lastCommonRoot + 1; x < fromDirectories.Length; x++)
				if (fromDirectories[x].Length > 0)
					relativePath.Add("..");

			// add to folders to path
			for (int x = lastCommonRoot + 1; x < toDirectories.Length; x++)
				relativePath.Add(toDirectories[x]);

			// create relative path
			string[] relativeParts = new string[relativePath.Count];

			relativePath.CopyTo(relativeParts, 0);

			string newPath = string.Join(
					Path.DirectorySeparatorChar.ToString(),
					relativeParts);

			return newPath;
		}


		/// <summary>
		/// Gets the schemas from XSD.
		/// </summary>
		/// <param name="filename">The filename.</param>
		/// <returns></returns>
		static public List<string> GetSchemasFromXsd(string filename)
		{
			return GetSchemasFromXsd(filename, null);
		}

		/// <summary>
		/// Gets the schemas from XSD.
		/// </summary>
		/// <param name="filename">The filename.</param>
		/// <param name="schemas">The schemas.</param>
		/// <returns></returns>
		static public List<string> GetSchemasFromXsd(string filename, Hashtable schemas)
		{
			filename = Path.GetFullPath(filename);
			if (schemas == null)
			{
				schemas = new Hashtable();
			}

			XmlSchemaSet set = new XmlSchemaSet();
			set.Add(XmlSchema.Read(XmlReader.Create(filename), null));
			set.Compile();

			foreach (XmlSchemaType obj in set.GlobalTypes.Values)
			{
				if (!string.IsNullOrEmpty(obj.SourceUri))
				{
					string expandedSchema = new Uri(obj.SourceUri).LocalPath;
					if (!schemas.Contains(expandedSchema.ToLower()) && File.Exists(expandedSchema))
					{
						schemas.Add(expandedSchema.ToLower(), expandedSchema);
					}
				}
			}
			List<string> list = new List<string>();
			foreach (string s in schemas.Values)
			{
				list.Add(s);
			}
			return list;
		}

		/// <summary>
		/// Gets the schemas from WSDL.
		/// </summary>
		/// <param name="filename">The filename.</param>
		/// <returns></returns>
		static public List<string> GetSchemasFromWsdl(string filename)
		{
			return GetSchemasFromWsdl(filename, null);
		}

		/// <summary>
		/// Gets the schemas from WSDL.
		/// </summary>
		/// <param name="filename">The filename.</param>
		/// <param name="schemas">The schemas.</param>
		/// <returns></returns>
		static public List<string> GetSchemasFromWsdl(string filename, Hashtable schemas)
		{
			if (schemas == null)
			{
				schemas = new Hashtable();
			}
			filename = Path.GetFullPath(filename);
			schemas.Add(filename.ToLower(), filename);
			ServiceDescription description = ServiceDescription.Read(filename);
			if (description != null && description.Types != null && description.Types.Schemas != null)
			{
				foreach (XmlSchema schema in description.Types.Schemas)
				{
					foreach (XmlSchemaImport schemaImport in schema.Includes)
					{
						string expandedSchema = schemaImport.SchemaLocation;
						if (!File.Exists(expandedSchema))
						{
							expandedSchema = Path.Combine(Path.GetDirectoryName(filename), expandedSchema);
						}
						if (!schemas.Contains(expandedSchema.ToLower()) && File.Exists(expandedSchema))
						{
							GetSchemasFromXsd(expandedSchema, schemas);
						}
					}
				}
			}
			List<string> list = new List<string>();
			foreach (string s in schemas.Values)
			{
				list.Add(s);
			}
			return list;
		}

		static public string GetWsdlTargetNamespace(string filename)
		{
			ServiceDescription description = ServiceDescription.Read(filename);
			return description.TargetNamespace;
		}

        static private string GetProgramFilesFolder()
        {
            string s = Environment.GetEnvironmentVariable("ProgramFiles(x86)");
            if (!Directory.Exists(s))
            {
                s = Environment.GetEnvironmentVariable("ProgramFiles");
            }
            return s;
        }

        static private string GetVcvarsall_bat()
		{
			string s = GetProgramFilesFolder();
			string[] folders = Directory.GetDirectories(s);
			foreach (string folder in folders)
			{
				if (folder.Contains("Microsoft Visual Studio"))
				{
					string file = Path.Combine(folder, @"VC\vcvarsall.bat");
					if (File.Exists(file))
					{
						return file;
					}
				}
			}
			throw new Exception("Can't find vcvarsall.bat. Visual Studio must be installed");
		}

        /// <summary>
		/// Generates the command file.
		/// </summary>
		/// <param name="cmdFile">The CMD file.</param>
		/// <param name="metadataFiles">The metadata files.</param>
		/// <param name="outputFile">The output file.</param>
		/// <param name="xmlNamespaceMappings">The XML namespace mappings.</param>
		/// <param name="referencedAssemblies">The referenced assemblies.</param>
		/// <param name="dataContractsOnly">if set to <c>true</c> [data contracts only].</param>
		/// <param name="language">The language.</param>
		/// <param name="configurationFile">The server configuration file.</param>
		static private void GenerateCommandFile(string cmdFile, List<string> metadataFiles, string outputFile,
			Dictionary<string, string> xmlNamespaceMappings, List<string> referencedAssemblies,
			bool dataContractsOnly, ClrLanguage language, string configurationFile, bool dataContractSerializer)
		{
			using (TextWriter tw = new StreamWriter(cmdFile, false))
			{
				tw.WriteLine("@echo off");

				string vcvarsallPath = GetVcvarsall_bat();

				if (!File.Exists(vcvarsallPath))
				{
					throw new Exception("Can't find " + vcvarsallPath);
				}
				tw.WriteLine("call \"{0}\"", vcvarsallPath);

				tw.WriteLine("@echo on");

				tw.Write("svcutil /l:{0} {1}",
					language.ToString(), dataContractSerializer ? "/ser:DataContractSerializer" : "");
				if (xmlNamespaceMappings != null)
				{
					foreach (string ns in xmlNamespaceMappings.Keys)
					{
						tw.Write(" /n:{0},{1}", ns, xmlNamespaceMappings[ns].Replace(" ", "_"));
					}
				}
				if (referencedAssemblies != null)
				{
					foreach (string referencedAssembly in referencedAssemblies)
					{
						tw.Write(" /r:\"{0}\"", Path.GetFullPath(referencedAssembly));
					}
				}
				if (!string.IsNullOrEmpty(outputFile))
				{
					tw.Write(" /out:\"{0}\"", outputFile);
				}
				if (dataContractsOnly)
				{
					tw.Write(" /dconly");
				}
				else
				{
					if (string.IsNullOrEmpty(configurationFile))
					{
						tw.Write(" /noconfig");
					}
					else
					{
						tw.Write(" /config:{0}", configurationFile);
						if (File.Exists(configurationFile))
						{
							tw.Write(" /mergeConfig");
						}
					}
				}
				if (metadataFiles != null)
				{
					foreach (string metadataFile in metadataFiles)
					{
						tw.Write(" \"{0}\"", metadataFile);
					}
				}
				tw.WriteLine();
				tw.Close();
			}
		}

		/// <summary>
		/// Runs the command file.
		/// </summary>
		/// <param name="cmdFile">The CMD file.</param>
		static private void RunCommandFile(string cmdFile)
		{
			Process p = new Process();
			p.StartInfo.FileName = cmdFile;
			p.StartInfo.RedirectStandardOutput = true;
			p.StartInfo.RedirectStandardError = true;
			p.EnableRaisingEvents = true;
			p.StartInfo.CreateNoWindow = true;
			p.StartInfo.UseShellExecute = false;
			p.ErrorDataReceived += proc_DataReceived;
			p.OutputDataReceived += proc_DataReceived;
			m_ConsoleOutput.Clear();
			p.Start();
			p.BeginErrorReadLine();
			p.BeginOutputReadLine();
			p.WaitForExit();
			p.Refresh();
			StringBuilder sb = new StringBuilder();
			string[] lines = m_ConsoleOutput.ToString().Split('\n');
			foreach (string line in lines)
			{
				if (line.ToLower().Contains("error"))
				{
					sb.AppendLine(line);
				}
			}
			if (p.ExitCode != 0 || m_ConsoleOutput.ToString().ToLower().Contains("error"))
			{
				throw new Exception(sb.ToString());
			}
		}

		static public string ConsoleOutput
		{
			get
			{
				return m_ConsoleOutput.ToString();
			}
		}

		static private StringBuilder m_ConsoleOutput = new StringBuilder();

		static void proc_DataReceived(object sender, DataReceivedEventArgs e)
		{
			if (e.Data != null)
			{
				lock (m_ConsoleOutput)
				{
					m_ConsoleOutput.AppendLine(e.Data);
				}
			}
		}

		/// <summary>
		/// Removes argument ReplyAction="*" from OperationContract attributes: [System.ServiceModel.OperationContractAttribute(Action="...", ReplyAction="*")]
		/// </summary>
		/// <param name="outputFile">The output file.</param>
		static private void RemoveDelayedBinding(string outputFile)
		{
			if (!string.IsNullOrEmpty(outputFile) && File.Exists(outputFile))
			{
				string code = string.Empty;
				using (StreamReader sr = new StreamReader(outputFile))
				{
					code = sr.ReadToEnd();
					sr.Close();
				}
				code = RemoveReplyAction(code);
				using (TextWriter tw = new StreamWriter(outputFile, false))
				{
					tw.Write(code);
					tw.Close();
				}
			}
		}

		static private string RemoveReplyAction(string wsdlData)
		{
			string stripped = string.Empty;
			return Regex.Replace(wsdlData, ", ReplyAction=\"\\*\"", "");
		}

		public static string GetSchemaNamespace(string xsdFile)
		{
			string ns = "";
			if (File.Exists(xsdFile))
			{
                try
                {
                    XmlSchema schema = XmlSchema.Read(XmlReader.Create(xsdFile), null);
                    ns = schema.TargetNamespace;
                }
                catch
                {
                    ns = Path.GetFileNameWithoutExtension(xsdFile);
                }
			}
			return ns;
		}

		public static string XmlNamespace2ClsNamespace(string xmlNamespace)
		{
			string clsNamespace = string.Empty;
			string[] parts = xmlNamespace.Replace('/', '.').Split(new char[] { '.' });
			foreach (string part in parts)
			{
				int i;
				string s = part.Trim();
				if (!int.TryParse(s, out i) && !string.IsNullOrEmpty(s) && s.ToUpper() != "HTTP:")
				{
					if (!string.IsNullOrEmpty(clsNamespace))
					{
						clsNamespace += ".";
					}
					clsNamespace += s;
				}
			}
			return clsNamespace;
		}

    }
}
